Проект: Анализ резюме из HeadHunter
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import plotly.express as px
import re
from plotly.subplots import make_subplots
Исследование структуры данных¶
- Прочитайте данные с помощью библиотеки Pandas. Совет: перед чтением обратите внимание на разделитель внутри файла.
#ссылка на данные для скачивания - https://cloud.mail.ru/public/DtQa/FDevv5AnK
dataSetHh = pd.read_csv('dst-3.0_16_1_hh_database.csv', sep=';')
- Выведите несколько первых (последних) строк таблицы, чтобы убедиться в том, что ваши данные не повреждены. Ознакомьтесь с признаками и их структурой.
#ваш код здесь
display(dataSetHh.head())
display(dataSetHh.tail())
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Мужчина , 39 лет , родился 27 ноября 1979 | 29000 руб. | Системный администратор | Советск (Калининградская область) , не готов к... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, ва... | Опыт работы 16 лет 10 месяцев Август 2010 — п... | МАОУ "СОШ № 1 г.Немана" | Системный администратор | Неоконченное высшее образование 2000 Балтийск... | 16.04.2019 15:59 | Имеется собственный автомобиль |
| 1 | Мужчина , 60 лет , родился 20 марта 1959 | 40000 руб. | Технический писатель | Королев , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, полный день, сменный график, уд... | Опыт работы 19 лет 5 месяцев Январь 2000 — по... | Временный трудовой коллектив | Менеджер проекта, Аналитик, Технический писатель | Высшее образование 1981 Военно-космическая ак... | 12.04.2019 08:42 | Не указано |
| 2 | Женщина , 36 лет , родилась 12 августа 1982 | 20000 руб. | Оператор | Тверь , не готова к переезду , не готова к ком... | полная занятость | полный день | Опыт работы 10 лет 3 месяца Октябрь 2004 — Де... | ПАО Сбербанк | Кассир-операционист | Среднее специальное образование 2002 Профессио... | 16.04.2019 08:35 | Не указано |
| 3 | Мужчина , 38 лет , родился 25 июня 1980 | 100000 руб. | Веб-разработчик (HTML / CSS / JS / PHP / базы ... | Саратов , не готов к переезду , готов к редким... | частичная занятость, проектная работа, полная ... | гибкий график, удаленная работа | Опыт работы 18 лет 9 месяцев Август 2017 — Ап... | OpenSoft | Инженер-программист | Высшее образование 2002 Саратовский государст... | 08.04.2019 14:23 | Не указано |
| 4 | Женщина , 26 лет , родилась 3 марта 1993 | 140000 руб. | Региональный менеджер по продажам | Москва , не готова к переезду , готова к коман... | полная занятость | полный день | Опыт работы 5 лет 7 месяцев Региональный мене... | Мармелад | Менеджер по продажам | Высшее образование 2015 Кгу Психологии и педаг... | 22.04.2019 10:32 | Не указано |
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 44739 | Мужчина , 30 лет , родился 17 января 1989 | 50000 руб. | Финансист, аналитик, экономист, бухгалтер, мен... | Тверь , готов к переезду (Москва, Химки) , гот... | полная занятость | полный день, удаленная работа | Опыт работы 7 лет 7 месяцев Финансист, аналит... | ООО "IAS" (независимый участник объединения Ru... | Руководитель субгруппы | Высшее образование 2015 Московский гуманитарн... | 22.04.2019 12:32 | Не указано |
| 44740 | Мужчина , 27 лет , родился 5 марта 1992 | 39000 руб. | Системный администратор, IT-специалист | Липецк , готов к переезду , готов к командировкам | проектная работа, частичная занятость, полная ... | удаленная работа, гибкий график, полный день, ... | Опыт работы 7 лет Системный администратор, IT... | ИП Пестрецов | Предприниматель | Высшее образование (Бакалавр) 2016 Воронежски... | 22.04.2019 13:11 | Не указано |
| 44741 | Женщина , 48 лет , родилась 26 декабря 1970 | 40000 руб. | Аналитик данных, Математик | Челябинск , готова к переезду , готова к редки... | полная занятость | полный день, удаленная работа | Опыт работы 21 год 5 месяцев Январь 1998 — по... | ОАО «ЧМК», Исследовательско-Технологический Це... | Начальник группы аналитики | Высшее образование 2000 Южно-Уральский госуда... | 09.04.2019 05:07 | Не указано |
| 44742 | Мужчина , 24 года , родился 6 октября 1994 | 20000 руб. | Контент-менеджер | Тамбов , не готов к переезду , не готов к кома... | частичная занятость, полная занятость | удаленная работа | Опыт работы 3 года 10 месяцев Контент-менедже... | IQ-Maxima | Менеджер проектов | Высшее образование 2015 Тамбовский государств... | 26.04.2019 14:25 | Имеется собственный автомобиль |
| 44743 | Мужчина , 38 лет , родился 25 апреля 1980 | 120000 руб. | Руководитель проекта | Москва , не готов к переезду , не готов к кома... | полная занятость | полный день | Опыт работы 15 лет 10 месяцев Руководитель пр... | ПАО ГК ТНС энерго | Руководитель отдела технической поддержки | Высшее образование 1997 Южно-Российский госуд... | 05.07.2018 20:15 | Не указано |
- Выведите основную информацию о числе непустых значений в столбцах и их типах в таблице.
- Обратите внимание на информацию о числе непустых значений.
#ваш код здесь
dataSetHh.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Пол, возраст 44744 non-null object 1 ЗП 44744 non-null object 2 Ищет работу на должность: 44744 non-null object 3 Город, переезд, командировки 44744 non-null object 4 Занятость 44744 non-null object 5 График 44744 non-null object 6 Опыт работы 44576 non-null object 7 Последнее/нынешнее место работы 44743 non-null object 8 Последняя/нынешняя должность 44742 non-null object 9 Образование и ВУЗ 44744 non-null object 10 Обновление резюме 44744 non-null object 11 Авто 44744 non-null object dtypes: object(12) memory usage: 4.1+ MB
- Выведите основную статистическую информацию о столбцах.
#ваш код здесь
dataSetHh.describe()
| Пол, возраст | ЗП | Ищет работу на должность: | Город, переезд, командировки | Занятость | График | Опыт работы | Последнее/нынешнее место работы | Последняя/нынешняя должность | Образование и ВУЗ | Обновление резюме | Авто | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 44744 | 44744 | 44744 | 44744 | 44744 | 44744 | 44576 | 44743 | 44742 | 44744 | 44744 | 44744 |
| unique | 16003 | 690 | 14929 | 10063 | 38 | 47 | 44413 | 30214 | 16927 | 40148 | 18838 | 2 |
| top | Мужчина , 32 года , родился 17 сентября 1986 | 50000 руб. | Системный администратор | Москва , не готов к переезду , не готов к кома... | полная занятость | полный день | Опыт работы 10 лет 8 месяцев Апрель 2018 — по... | Индивидуальное предпринимательство / частная п... | Системный администратор | Высшее образование 1987 Военный инженерный Кра... | 07.05.2019 09:50 | Не указано |
| freq | 18 | 4064 | 3099 | 1261 | 30026 | 22727 | 3 | 935 | 2062 | 4 | 25 | 32268 |
Преобразование данных¶
- Начнем с простого - с признака "Образование и ВУЗ". Его текущий формат это: <Уровень образования год выпуска ВУЗ специальность...>. Например:
- Высшее образование 2016 Московский авиационный институт (национальный исследовательский университет)...
- Неоконченное высшее образование 2000 Балтийская государственная академия рыбопромыслового флота…
Нас будет интересовать только уровень образования.
Создайте с помощью функции-преобразования новый признак "Образование", который должен иметь 4 категории: "высшее", "неоконченное высшее", "среднее специальное" и "среднее".
Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Образование и ВУЗ".
Совет: обратите внимание на структуру текста в столбце "Образование и ВУЗ". Гарантируется, что текущий уровень образования соискателя всегда находится в первых 2ух слов и начинается с заглавной буквы. Воспользуйтесь этим.
Совет: проверяйте полученные категории, например, с помощью метода unique()
#ваш код здесь
def getTypeEducation(str):
splitted = str.split(" ")
stringForSearch = (splitted[0] + ' ' + splitted[1]).lower()
return stringForSearch.replace('образование','').strip()
dataSetHh['Образование'] = dataSetHh['Образование и ВУЗ'].apply(lambda x: getTypeEducation(x))
dataSetHh.drop('Образование и ВУЗ', axis=1, inplace=True)
dataSetHh.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Пол, возраст 44744 non-null object 1 ЗП 44744 non-null object 2 Ищет работу на должность: 44744 non-null object 3 Город, переезд, командировки 44744 non-null object 4 Занятость 44744 non-null object 5 График 44744 non-null object 6 Опыт работы 44576 non-null object 7 Последнее/нынешнее место работы 44743 non-null object 8 Последняя/нынешняя должность 44742 non-null object 9 Обновление резюме 44744 non-null object 10 Авто 44744 non-null object 11 Образование 44744 non-null object dtypes: object(12) memory usage: 4.1+ MB
- Теперь нас интересует столбец "Пол, возраст". Сейчас он представлен в формате <Пол , возраст , дата рождения >. Например:
- Мужчина , 39 лет , родился 27 ноября 1979
- Женщина , 21 год , родилась 13 января 2000
Как вы понимаете, нам необходимо выделить каждый параметр в отдельный столбец.
Создайте два новых признака "Пол" и "Возраст". При этом важно учесть:
- Признак пола должен иметь 2 уникальных строковых значения: 'М' - мужчина, 'Ж' - женщина.
- Признак возраста должен быть представлен целыми числами.
Выполните преобразование, ответьте на контрольные вопросы и удалите признак "Пол, возраст" из таблицы.
Совет: обратите внимание на структуру текста в столбце, в части на то, как разделены параметры пола, возраста и даты рождения между собой - символом ' , '. Гарантируется, что структура одинакова для всех строк в таблице. Вы можете воспользоваться этим.
#ваш код здесь
def splitGenderAge(str, key = 0):
splitedString = str.split(" , ")
if key == 0:
return splitedString[key].strip()[0].upper()
return int(splitedString[key].strip().split(" ")[0])
dataSetHh['Пол'] = dataSetHh['Пол, возраст'].apply(lambda x: splitGenderAge(x))
dataSetHh['Возраст'] = dataSetHh['Пол, возраст'].apply(lambda x: splitGenderAge(x,1))
dataSetHh.drop('Пол, возраст', axis=1, inplace=True)
meanAge = dataSetHh['Возраст'].mean()
percentWoman = dataSetHh[dataSetHh['Пол'] == 'Ж']['Пол'].count()/dataSetHh['Пол'].count() * 100
print('Средний возраст соискателей {}'.format(meanAge))
print('Среди всех соискателей {}% - женщины'.format(str(percentWoman)))
Средний возраст соискателей 32.19674146254246 Среди всех соискателей 19.070713391739673% - женщины
- Следующим этапом преобразуем признак "Опыт работы". Его текущий формат - это: <Опыт работы: n лет m месяцев, периоды работы в различных компаниях…>.
Из столбца нам необходимо выделить общий опыт работы соискателя в месяцах, новый признак назовем "Опыт работы (месяц)"
Для начала обсудим условия решения задачи:
- Во-первых, в данном признаке есть пропуски. Условимся, что если мы встречаем пропуск, оставляем его как есть (функция-преобразование возвращает NaN)
- Во-вторых, в данном признаке есть скрытые пропуски. Для некоторых соискателей в столбце стоит значения "Не указано". Их тоже обозначим как NaN (функция-преобразование возвращает NaN)
- В-третьих, нас не интересует информация, которая описывается после указания опыта работы (периоды работы в различных компаниях)
- В-четвертых, у нас есть проблема: опыт работы может быть представлен только в годах или только месяцах. Например, можно встретить следующие варианты:
- Опыт работы 3 года 2 месяца…
- Опыт работы 4 года…
- Опыт работы 11 месяцев…
- Учитывайте эту особенность в вашем коде
Учитывайте эту особенность в вашем коде
В результате преобразования у вас должен получиться столбец, содержащий информацию о том, сколько месяцев проработал соискатель. Выполните преобразование, ответьте на контрольные вопросы и удалите столбец "Опыт работы" из таблицы.
#ваш код здесь
def getWorkedMonth(string):
if str(string) in ['nan', 'Не указано']:
return np.nan
res = list(re.findall(r'Опыт работы (\d+) (\w+) (\d+ (\w+)|)',string)[0])
count = int(res[0])
if 'мес' not in res[1]:
count *= 12
if(str(res[2]) == 'nan'):
return count
if res[2] != '':
#return int(res[2].split(' ')[0])
count += int(res[2].split(' ')[0])
return count
dataSetHh['Опыт работы (месяц)'] = dataSetHh['Опыт работы'].apply(lambda x: getWorkedMonth(x))
dataSetHh.drop('Опыт работы', axis=1, inplace=True)
dataSetHh['Опыт работы (месяц)'].median()
100.0
- Хорошо идем! Следующий на очереди признак "Город, переезд, командировки". Информация в нем представлена в следующем виде: <Город , (метро) , готовность к переезду (города для переезда) , готовность к командировкам>. В скобках указаны необязательные параметры строки. Например, можно встретить следующие варианты:
- Москва , не готов к переезду , готов к командировкам
- Москва , м. Беломорская , не готов к переезду, не готов к командировкам
- Воронеж , готов к переезду (Сочи, Москва, Санкт-Петербург) , готов к командировкам
Создадим отдельные признаки "Город", "Готовность к переезду", "Готовность к командировкам". При этом важно учесть:
Признак "Город" должен содержать только 4 категории: "Москва", "Санкт-Петербург" и "город-миллионник" (их список ниже), остальные обозначьте как "другие".
Список городов-миллионников:
Инфорация о метро, рядом с которым проживает соискатель нас не интересует.million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']Признак "Готовность к переезду" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к переезду в признаке "Город, переезд, командировки". Например:
- … , готов к переезду , …
- … , не готова к переезду , …
- … , готова к переезду (Москва, Санкт-Петербург, Ростов-на-Дону)
- … , хочу переехать (США) , …
Нас интересует только сам факт возможности или желания переезда.
Признак "Готовность к командировкам" должен иметь два возможных варианта: True или False. Обратите внимание, что возможны несколько вариантов описания готовности к командировкам в признаке "Город, переезд, командировки". Например:
- … , готов к командировкам , …
- … , готова к редким командировкам , …
- … , не готов к командировкам , …
Нас интересует только сам факт готовности к командировке.
Еще один важный факт: при выгрузки данных у некоторых соискателей "потерялась" информация о готовности к командировкам. Давайте по умолчанию будем считать, что такие соискатели не готовы к командировкам.
Выполните преобразования и удалите столбец "Город, переезд, командировки" из таблицы.
Совет: обратите внимание на то, что структура текста может меняться в зависимости от указания ближайшего метро. Учите это, если будете использовать порядок слов в своей программе.
#ваш код здесь
million_cities = ['Новосибирск', 'Екатеринбург','Нижний Новгород','Казань', 'Челябинск','Омск', 'Самара', 'Ростов-на-Дону', 'Уфа', 'Красноярск', 'Пермь', 'Воронеж','Волгоград']
def getCity(string):
res = string.split(',')[0].strip()
if res in ["Москва", "Санкт-Петербург"]:
return res
if res in million_cities:
return 'город-миллионник'
return 'другие'
def getRelocation(string):
res = re.findall(r', (готов к перее|готова к перее|хочу перее)',string)
return len(res) > 0
def getTrips(string):
res = re.findall(r', (готов|готова) к (?:\w+ |)коман',string)
return len(res) > 0
dataSetHh['Город'] = dataSetHh['Город, переезд, командировки'].apply(getCity)
dataSetHh['Готовность к переезду'] = dataSetHh['Город, переезд, командировки'].apply(getRelocation)
dataSetHh['Готовность к командировкам'] = dataSetHh['Город, переезд, командировки'].apply(getTrips)
dataSetHh[dataSetHh['Город'] == 'Санкт-Петербург']['Город'].count()/dataSetHh['Город'].count() * 100
dataSetHh[(dataSetHh['Готовность к переезду']) & (dataSetHh['Готовность к командировкам'])]['ЗП'].count()/dataSetHh['Город'].count() * 100
dataSetHh.drop('Город, переезд, командировки', axis=1, inplace=True)
- Рассмотрим поближе признаки "Занятость" и "График". Сейчас признаки представляют собой набор категорий желаемой занятости (полная занятость, частичная занятость, проектная работа, волонтерство, стажировка) и желаемого графика работы (полный день, сменный график, гибкий график, удаленная работа, вахтовый метод).
На сайте hh.ru соискатель может указывать различные комбинации данных категорий, например:
- полная занятость, частичная занятость
- частичная занятость, проектная работа, волонтерство
- полный день, удаленная работа
- вахтовый метод, гибкий график, удаленная работа, полная занятость
Такой вариант признаков имеет множество различных комбинаций, а значит множество уникальных значений, что мешает анализу. Нужно это исправить!
Давайте создадим признаки-мигалки для каждой категории: если категория присутствует в списке желаемых соискателем, то в столбце на месте строки рассматриваемого соискателя ставится True, иначе - False.
Такой метод преобразования категориальных признаков называется One Hot Encoding и его схема представлена на рисунке ниже:
Выполните данное преобразование для признаков "Занятость" и "График", ответьте на контрольные вопросы, после чего удалите их из таблицы
#ваш код здесь
dataSetHh['полная занятость'] = dataSetHh['Занятость'].apply(lambda x: 'полная' in x)
dataSetHh['полный день'] = dataSetHh['График'].apply(lambda x: 'полный' in x)
dataSetHh['частичная занятость'] = dataSetHh['Занятость'].apply(lambda x: 'частичная' in x)
dataSetHh['сменный график'] = dataSetHh['График'].apply(lambda x: 'сменный' in x)
dataSetHh['проектная работа'] = dataSetHh['Занятость'].apply(lambda x: 'проектн' in x)
dataSetHh['гибкий график'] = dataSetHh['График'].apply(lambda x: 'гибкий' in x)
dataSetHh['волонтерство'] = dataSetHh['Занятость'].apply(lambda x: 'волонтер' in x)
dataSetHh['удаленная работа'] = dataSetHh['График'].apply(lambda x: 'удален' in x)
dataSetHh['стажировка'] = dataSetHh['Занятость'].apply(lambda x: 'стажиров' in x)
dataSetHh['вахтовый метод'] = dataSetHh['График'].apply(lambda x: 'вахтовый' in x)
print(dataSetHh[dataSetHh['проектная работа'] & dataSetHh['волонтерство']]['ЗП'].count())
print(dataSetHh[dataSetHh['вахтовый метод'] & dataSetHh['гибкий график']]['ЗП'].count())
dataSetHh.drop(['Занятость','График'], axis=1, inplace=True)
436 2311
- (2 балла) Наконец, мы добрались до самого главного и самого важного - признака заработной платы "ЗП".
В чем наша беда? В том, что помимо желаемой заработной платы соискатель указывает валюту, в которой он бы хотел ее получать, например:
- 30000 руб.
- 50000 грн.
- 550 USD
Нам бы хотелось видеть заработную плату в единой валюте, например, в рублях. Возникает вопрос, а где взять курс валют по отношению к рублю?
На самом деле язык Python имеет в арсенале огромное количество возможностей получения данной информации, от обращения к API Центробанка, до использования специальных библиотек, например pycbrf. Однако, это не тема нашего проекта.
Поэтому мы пойдем в лоб: обратимся к специальным интернет-ресурсам для получения данных о курсе в виде текстовых файлов. Например, MDF.RU, данный ресурс позволяет удобно экспортировать данные о курсах различных валют и акций за указанные периоды в виде csv файлов. Мы уже сделали выгрузку курсов валют, которые встречаются в наших данных за период с 29.12.2017 по 05.12.2019. Скачать ее вы можете на платформе
Создайте новый DataFrame из полученного файла. В полученной таблице нас будут интересовать столбцы:
- "currency" - наименование валюты в ISO кодировке,
- "date" - дата,
- "proportion" - пропорция,
- "close" - цена закрытия (последний зафиксированный курс валюты на указанный день).
Перед вами таблица соответствия наименований иностранных валют в наших данных и их общепринятых сокращений, которые представлены в нашем файле с курсами валют. Пропорция - это число, за сколько единиц валюты указан курс в таблице с курсами. Например, для казахстанского тенге курс на 20.08.2019 составляет 17.197 руб. за 100 тенге, тогда итоговый курс равен - 17.197 / 100 = 0.17197 руб за 1 тенге. Воспользуйтесь этой информацией в ваших преобразованиях.
Осталось только понять, откуда брать дату, по которой определяется курс? А вот же она - в признаке "Обновление резюме", в нем содержится дата и время, когда соискатель выложил текущий вариант своего резюме. Нас интересует только дата, по ней бы и будем сопоставлять курсы валют.
Теперь у нас есть вся необходимая информация для того, чтобы создать признак "ЗП (руб)" - заработная плата в рублях.
После ответа на контрольные вопросы удалите исходный столбец заработной платы "ЗП" и все промежуточные столбцы, если вы их создавали.
Итак, давайте обсудим возможный алгоритм преобразования:
- Перевести признак "Обновление резюме" из таблицы с резюме в формат datetime и достать из него дату. В тот же формат привести признак "date" из таблицы с валютами.
- Выделить из столбца "ЗП" сумму желаемой заработной платы и наименование валюты, в которой она исчисляется. Наименование валюты перевести в стандарт ISO согласно с таблицей выше.
- Присоединить к таблице с резюме таблицу с курсами по столбцам с датой и названием валюты (подумайте, какой тип объединения надо выбрать, чтобы в таблице с резюме сохранились данные о заработной плате, изначально представленной в рублях). Значение close для рубля заполнить единицей 1 (курс рубля самого к себе)
- Умножить сумму желаемой заработной платы на присоединенный курс валюты (close) и разделить на пропорцию (обратите внимание на пропуски после объединения в этих столбцах), результат занести в новый столбец "ЗП (руб)".
#ваш код здесь
dataValuteRate = pd.read_csv('ExchangeRates.csv') #ссылка на скачивание - https://cloud.mail.ru/public/cJcY/GH8PhVNy4
dataValuteRate.drop('per', axis=1, inplace=True)
dataValuteRate.drop('time', axis=1, inplace=True)
dataValuteRate.drop('vol', axis=1, inplace=True)
dataValuteRate['date'] = pd.to_datetime(dataValuteRate['date'], dayfirst=True).dt.date
dataSetHh['Обновление резюме'] = pd.to_datetime(dataSetHh['Обновление резюме'], dayfirst=True).dt.date
dataSetHh[['ЗП сумма', 'ЗП валюта']] = dataSetHh['ЗП'].str.split(' ', expand=True)
def changeStrValute(string):
currencyDict = {'руб':'RUB', 'белруб':'BYN', 'KZT':'KZT', 'EUR':'EUR', 'USD':'USD', 'грн':'UAH', 'сум':'UZS', 'KGS':'KGS', 'AZN':'AZN'}
curr = string.replace('.', '')
if curr in currencyDict:
return currencyDict.get(curr)
return np.nan
#display(dataSetHh['ЗП валюта'].unique())
dataSetHh['ЗП валюта'] = dataSetHh['ЗП валюта'].apply(changeStrValute)
mergedDataSetHh = dataSetHh.merge(right=dataValuteRate, how='left', left_on=['ЗП валюта','Обновление резюме'], right_on=['currency', 'date'])
mergedDataSetHh.fillna(value={'proportion': 1, 'close': 1}, inplace=True)
mergedDataSetHh['ЗП (руб)'] = mergedDataSetHh['ЗП сумма'].astype(float) * mergedDataSetHh['close'] / mergedDataSetHh['proportion']
mergedDataSetHh.drop(['ЗП', 'ЗП сумма', 'ЗП валюта', 'close', 'proportion','date','currency'], axis=1, inplace=True)
print(round(mergedDataSetHh['ЗП (руб)'].median()/1000))
mergedDataSetHh.info()
/var/folders/zr/fdqqpt_d6g3__hn98sky8b400000gn/T/ipykernel_31487/2881895306.py:6: UserWarning: Could not infer format, so each element will be parsed individually, falling back to `dateutil`. To ensure parsing is consistent and as-expected, please specify a format. dataValuteRate['date'] = pd.to_datetime(dataValuteRate['date'], dayfirst=True).dt.date
59 <class 'pandas.core.frame.DataFrame'> RangeIndex: 44744 entries, 0 to 44743 Data columns (total 23 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Ищет работу на должность: 44744 non-null object 1 Последнее/нынешнее место работы 44743 non-null object 2 Последняя/нынешняя должность 44742 non-null object 3 Обновление резюме 44744 non-null object 4 Авто 44744 non-null object 5 Образование 44744 non-null object 6 Пол 44744 non-null object 7 Возраст 44744 non-null int64 8 Опыт работы (месяц) 44574 non-null float64 9 Город 44744 non-null object 10 Готовность к переезду 44744 non-null bool 11 Готовность к командировкам 44744 non-null bool 12 полная занятость 44744 non-null bool 13 полный день 44744 non-null bool 14 частичная занятость 44744 non-null bool 15 сменный график 44744 non-null bool 16 проектная работа 44744 non-null bool 17 гибкий график 44744 non-null bool 18 волонтерство 44744 non-null bool 19 удаленная работа 44744 non-null bool 20 стажировка 44744 non-null bool 21 вахтовый метод 44744 non-null bool 22 ЗП (руб) 44744 non-null float64 dtypes: bool(12), float64(2), int64(1), object(8) memory usage: 4.3+ MB
Исследование зависимостей в данных¶
- Постройте распределение признака "Возраст". Опишите распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится возраст большинства соискателей? Есть ли аномалии для признака возраста, какие значения вы бы причислили к их числу?
Совет: постройте гистограмму и коробчатую диаграмму рядом.
# ваш код здесь
def printHistogram(data,x,yName):
hist = px.histogram(
data_frame=data,
x=x,
histfunc='count'
).update_layout(yaxis_title=yName)
hist.show()
def printBox(data, x):
box = px.box(
data_frame=data,
x=x,
)
box.show()
printHistogram(mergedDataSetHh,'Возраст','Количество')
printBox(mergedDataSetHh,"Возраст")
1 - мода распределения - 30 лет
2 - предельные значения - 14 и 100 лет
3 - примерный интервал большинства соискателей от 27 до 36 лет
4 - в аномалии по коробчатой диаграмме попали все соискатели старше 49 лет, однако работать могут люди и старше, и даже на песнии. В реальности аномальным возрастом соискателя является - 100 лет
- Постройте распределение признака "Опыт работы (месяц)". Опишите данное распределение, отвечая на следующие вопросы: чему равна мода распределения, каковы предельные значения признака, в каком примерном интервале находится опыт работы большинства соискателей? Есть ли аномалии для признака опыта работы, какие значения вы бы причислили к их числу?
Совет: постройте гистограмму и коробчатую диаграмму рядом.
# ваш код здесь
printHistogram(mergedDataSetHh,'Опыт работы (месяц)','Количество')
printBox(mergedDataSetHh,"Опыт работы (месяц)")
1 - мода распределения - 80-84 месяца
2 - пределные значения 1 и 1188 месяцв
3 - примерный интерфал - от 57 до 154 месяцев
4 - в аномалии попали все с опытом работы выше 300 месяцев, однако действительно аномальынм результатом можно считать 1188 месяцев (100 лет)
- Постройте распределение признака "ЗП (руб)". Опишите данное распределение, отвечая на следующие вопросы: каковы предельные значения признака, в каком примерном интервале находится заработная плата большинства соискателей? Есть ли аномалии для признака возраста? Обратите внимание на гигантские размеры желаемой заработной платы.
Совет: постройте гистограмму и коробчатую диаграмму рядом.
printHistogram(mergedDataSetHh,'ЗП (руб)','Количество')
printBox(mergedDataSetHh,"ЗП (руб)")
1 - предельные признаки от 1 рубля до 24.3 миллиона рублей
2 - примерный интервал - от 37 до 180.1 тысячи рублей
3 - аномалией можно считать зарплату в 1 рубль и зарплату выше 1 миллиона рублей
- Постройте диаграмму, которая показывает зависимость медианной желаемой заработной платы ("ЗП (руб)") от уровня образования ("Образование"). Используйте для диаграммы данные о резюме, где желаемая заработная плата меньше 1 млн рублей.
Сделайте выводы по представленной диаграмме: для каких уровней образования наблюдаются наибольшие и наименьшие уровни желаемой заработной платы? Как вы считаете, важен ли признак уровня образования при прогнозировании заработной платы?
# ваш код здесь
filteredData = mergedDataSetHh[mergedDataSetHh['ЗП (руб)'] < 1000000]
medianData = filteredData.groupby('Образование')[['ЗП (руб)']].median().reset_index()
fig = px.bar(
data_frame=medianData,
x="Образование",
y="ЗП (руб)",
text = 'ЗП (руб)',
title='Зависимость медианной желаемой заработной платы от уровня образования' #заголовок
)
fig.show()
1 - наибольшие зарплаты ожидаются у соискателей с высшим образованием, наименьшие - со школьным и средне-специальным (колледж)
2 - считаю что признак образования важен, но не критичен, куда важнее опыт работы
- Постройте диаграмму, которая показывает распределение желаемой заработной платы ("ЗП (руб)") в зависимости от города ("Город"). Используйте для диаграммы данные о резюме, где желая заработная плата меньше 1 млн рублей.
Сделайте выводы по полученной диаграмме: как соотносятся медианные уровни желаемой заработной платы и их размах в городах? Как вы считаете, важен ли признак города при прогнозировании заработной платы?
# ваш код здесь
fig = px.box(
data_frame=filteredData,
x='ЗП (руб)',
y='Город',
title='Распределение желаемой заработной платы в зависимости от города'
)
fig.show()
Медианные зарплаты по городам соотносятся как и ожидалось, в Москве и Санкт-петербурге они выше всего - 85 и 60 тысяч рублей, в остальных городах-миллионниках и оставшихся городах медианная зарплата одинаковая. При этом самый болшой размах зарплат в других городах, но только за счет максимального значения в 923.9 тысяч рублей. Затем уже идет Мсоква, Санкт-Петербург и остальные города-миллионники. Город важен так как в крупных городах жизнь дороже, соответственно и зарплата должна быть выше.
- Постройте многоуровневую столбчатую диаграмму, которая показывает зависимость медианной заработной платы ("ЗП (руб)") от признаков "Готовность к переезду" и "Готовность к командировкам". Проанализируйте график, сравнив уровень заработной платы в категориях.
# ваш код здесь
medianZPRelocate = filteredData.groupby(['Готовность к переезду', 'Готовность к командировкам'])['ЗП (руб)'].median().reset_index()
fig = px.bar(medianZPRelocate,
x='Готовность к переезду',
y='ЗП (руб)',
facet_col='Готовность к командировкам',
width=800)
fig.show()
- Постройте сводную таблицу, иллюстрирующую зависимость медианной желаемой заработной платы от возраста ("Возраст") и образования ("Образование"). На полученной сводной таблице постройте тепловую карту. Проанализируйте тепловую карту, сравнив показатели внутри групп.
# ваш код здесь
ageEductationData = filteredData.groupby(['Возраст', 'Образование'])['ЗП (руб)'].median().reset_index()
fig = px.density_heatmap(
data_frame=ageEductationData,
x='Возраст',
y='Образование',
z='ЗП (руб)',
histfunc='avg'
)
fig.show()
Самая высокая ожидаемая ЗП у людей с высшим образованием в возрасте 40-49 лет
Соискатели со средне-специальным образованием хотят ЗП ниже, чем со средним обращованием во всех возрастах
- Постройте диаграмму рассеяния, показывающую зависимость опыта работы ("Опыт работы (месяц)") от возраста ("Возраст"). Опыт работы переведите из месяцев в года, чтобы признаки были в едином масштабе. Постройте на графике дополнительно прямую, проходящую через точки (0, 0) и (100, 100). Данная прямая соответствует значениям, когда опыт работы равен возрасту человека. Точки, лежащие на этой прямой и выше нее - аномалии в наших данных (опыт работы больше либо равен возрасту соискателя)
# ваш код здесь
filteredData = filteredData.reset_index(drop=True)
filteredData['Опыт работы (год)'] = (filteredData['Опыт работы (месяц)'] / 12)
fig = px.scatter(
data_frame=filteredData,
x='Возраст',
y='Опыт работы (год)',
title='Зависимость опыта работы от возраста'
)
# Добавляем прямую линию
fig.add_shape(type="line",
x0=0, y0=0,
x1=100, y1=100,)
fig.show()
На графике 7 точек-аномалий, где опыт больше возраста, и одна с возрастом 100 и опытом 2.5 года
По графику можно увидеть что чем выше опыт работы, тем выше диапазон запроса, но при этом минимальная ожидаемая зарплата правтически не меняется
Дополнительные баллы
Для получения 2 дополнительных баллов по разведывательному анализу постройте еще два любых содержательных графика или диаграммы, которые помогут проиллюстрировать влияние признаков/взаимосвязь между признаками/распределения признаков. Приведите выводы по ним. Желательно, чтобы в анализе участвовали признаки, которые мы создавали ранее в разделе "Преобразование данных".
# ваш код здесь
ваши выводы здесь
Очистка данных¶
- Начнем с дубликатов в наших данных. Найдите полные дубликаты в таблице с резюме и удалите их.
# ваш код здесь
clearedData = mergedDataSetHh.copy()
count = clearedData[clearedData.duplicated()].shape[0]
clearedData.drop_duplicates(inplace=True)
print(count)
161
- Займемся пропусками. Выведите информацию о числе пропусков в столбцах.
# ваш код здесь
searchNulls = clearedData.isna().sum()
searchNulls[searchNulls>0]
Последнее/нынешнее место работы 1 Последняя/нынешняя должность 2 Опыт работы (месяц) 168 dtype: int64
- Итак, у нас есть пропуски в 3ех столбцах: "Опыт работы (месяц)", "Последнее/нынешнее место работы", "Последняя/нынешняя должность". Поступим следующим образом: удалите строки, где есть пропуск в столбцах с местом работы и должностью. Пропуски в столбце с опытом работы заполните медианным значением.
# ваш код здесь
clearedData.dropna(subset=['Последнее/нынешнее место работы', 'Последняя/нынешняя должность'], inplace=True)
clearedData.fillna(value={'Опыт работы (месяц)': clearedData['Опыт работы (месяц)'].median()},inplace=True)
clearedData['Опыт работы (месяц)'].mean()
114.35777573405711
- Мы добрались до ликвидации выбросов. Сначала очистим данные вручную. Удалите резюме, в которых указана заработная плата либо выше 1 млн. рублей, либо ниже 1 тыс. рублей.
# ваш код здесь
clearedData = clearedData[(clearedData['ЗП (руб)'] <= 10**6) & (clearedData['ЗП (руб)'] >= 10**3)]
- В процессе разведывательного анализа мы обнаружили резюме, в которых опыт работы в годах превышал возраст соискателя. Найдите такие резюме и удалите их из данных
clearedData = clearedData[(clearedData['Опыт работы (месяц)'] / 12 <= clearedData['Возраст'])]
- В результате анализа мы обнаружили потенциальные выбросы в признаке "Возраст". Это оказались резюме людей чересчур преклонного возраста для поиска работы. Попробуйте построить распределение признака в логарифмическом масштабе. Добавьте к графику линии, отображающие среднее и границы интервала метода трех сигм. Напомним, сделать это можно с помощью метода axvline. Например, для построение линии среднего будет иметь вид:
histplot.axvline(log_age.mean(), color='k', lw=2)
В какую сторону асимметрично логарифмическое распределение? Напишите об этом в комментарии к графику. Найдите выбросы с помощью метода z-отклонения и удалите их из данных, используйте логарифмический масштаб. Давайте сделаем послабление на 1 сигму (возьмите 4 сигмы) в правую сторону.
Выведите таблицу с полученными выбросами и оцените, с каким возрастом соискатели попадают под категорию выбросов?
# ваш код здесь
log_age = np.log(clearedData['Возраст'] +1)
histplot = sns.histplot(log_age , bins=30)
histplot.axvline(log_age.mean(), color='k', lw=2)
histplot.axvline(log_age.mean() + 3 * log_age.std(), color='k', lw=2)
histplot.axvline(log_age.mean() - 3 * log_age.std(), color='k', lw=2)
<matplotlib.lines.Line2D at 0x14a07d0a0>
Справа от моды видим большее количество наблюдений, поэтому логарифмическое распределение имеет правостороннюю асимметрию.
def outZClear(data, left=3, right=3):
x = np.log(data['Возраст']+1)
mean = x.mean()
sigma = x.std()
lowerBound = mean - left * sigma
upperBound = mean + right * sigma
out = data[(x < lowerBound) | (x > upperBound)]
cleaned = data[(x >= lowerBound) & (x <= upperBound)]
return [out, cleaned]
results = outZClear(clearedData, right=4)
display(results[0])
display(results[1])
| Ищет работу на должность: | Последнее/нынешнее место работы | Последняя/нынешняя должность | Обновление резюме | Авто | Образование | Пол | Возраст | Опыт работы (месяц) | Город | ... | полный день | частичная занятость | сменный график | проектная работа | гибкий график | волонтерство | удаленная работа | стажировка | вахтовый метод | ЗП (руб) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 31137 | Менеджер по работе с клиентами | ООО "ФёрстКэшКомпани" | Менеджер по работе с клиентами | 2019-04-06 | Не указано | среднее | М | 15 | 2.0 | Санкт-Петербург | ... | False | True | True | False | True | False | True | False | False | 10000.0 |
| 32950 | Тестировщик игр | OOO ЖМЫХ | Тестировщик ПО | 2019-04-09 | Не указано | среднее специальное | М | 15 | 3.0 | другие | ... | True | False | False | False | False | False | False | False | False | 2000.0 |
| 33654 | Frontend-разработчик | Freelance | Frontend-разработчик | 2019-04-19 | Не указано | среднее специальное | М | 100 | 30.0 | Санкт-Петербург | ... | True | True | False | True | True | False | True | True | False | 60000.0 |
3 rows × 23 columns
| Ищет работу на должность: | Последнее/нынешнее место работы | Последняя/нынешняя должность | Обновление резюме | Авто | Образование | Пол | Возраст | Опыт работы (месяц) | Город | ... | полный день | частичная занятость | сменный график | проектная работа | гибкий график | волонтерство | удаленная работа | стажировка | вахтовый метод | ЗП (руб) | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Системный администратор | МАОУ "СОШ № 1 г.Немана" | Системный администратор | 2019-04-16 | Имеется собственный автомобиль | неоконченное высшее | М | 39 | 202.0 | другие | ... | True | True | True | True | True | False | True | False | True | 29000.0 |
| 1 | Технический писатель | Временный трудовой коллектив | Менеджер проекта, Аналитик, Технический писатель | 2019-04-12 | Не указано | высшее | М | 60 | 233.0 | другие | ... | True | True | True | True | True | False | True | False | False | 40000.0 |
| 2 | Оператор | ПАО Сбербанк | Кассир-операционист | 2019-04-16 | Не указано | среднее специальное | Ж | 36 | 123.0 | другие | ... | True | False | False | False | False | False | False | False | False | 20000.0 |
| 3 | Веб-разработчик (HTML / CSS / JS / PHP / базы ... | OpenSoft | Инженер-программист | 2019-04-08 | Не указано | высшее | М | 38 | 225.0 | другие | ... | False | True | False | True | True | False | True | False | False | 100000.0 |
| 4 | Региональный менеджер по продажам | Мармелад | Менеджер по продажам | 2019-04-22 | Не указано | высшее | Ж | 26 | 67.0 | Москва | ... | True | False | False | False | False | False | False | False | False | 140000.0 |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 44739 | Финансист, аналитик, экономист, бухгалтер, мен... | ООО "IAS" (независимый участник объединения Ru... | Руководитель субгруппы | 2019-04-22 | Не указано | высшее | М | 30 | 91.0 | другие | ... | True | False | False | False | False | False | True | False | False | 50000.0 |
| 44740 | Системный администратор, IT-специалист | ИП Пестрецов | Предприниматель | 2019-04-22 | Не указано | высшее | М | 27 | 84.0 | другие | ... | True | True | True | True | True | False | True | False | False | 39000.0 |
| 44741 | Аналитик данных, Математик | ОАО «ЧМК», Исследовательско-Технологический Це... | Начальник группы аналитики | 2019-04-09 | Не указано | высшее | Ж | 48 | 257.0 | город-миллионник | ... | True | False | False | False | False | False | True | False | False | 40000.0 |
| 44742 | Контент-менеджер | IQ-Maxima | Менеджер проектов | 2019-04-26 | Имеется собственный автомобиль | высшее | М | 24 | 46.0 | другие | ... | False | True | False | False | False | False | True | False | False | 20000.0 |
| 44743 | Руководитель проекта | ПАО ГК ТНС энерго | Руководитель отдела технической поддержки | 2018-07-05 | Не указано | высшее | М | 38 | 190.0 | Москва | ... | True | False | False | False | False | False | False | False | False | 120000.0 |
44482 rows × 23 columns